#!/usr/bin/env python
#
# Routines for reading simulator data files in the common format produced
# by the common_writer input module.
#
# Author:   Mike Murphy <mamurph@cs.clemson.edu>
# Revision: 23 September 2009
#
#   Copyright 2009 Clemson University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.

'''
Common routines for reading simulator input files in the common format
produced by simvoc.input.common_writer. This module implements handlers
for the elements that are common to all format types, such as metadata,
while subclasses will add handlers for specific input formats.
'''

from results import MetadataResult


class ReaderException(Exception):
   '''
   Exception thrown when scan or parse errors occur in an input file
   '''
   pass
#


class CommonReader(object):
   '''
   Implements a common scanner and parser for the simulator input formats
   written by simvoc.input.common_writer. This base class handles basic
   file syntax checking, comment stripping, format number checking, clock
   lines, and metadata. In addition, this implementation automatically
   schedules the parsing of clock blocks at the appropriate times specified
   by the clock lines. Subclasses will add parse handlers and extend the
   parser map to handle commands specific to different types of files.

   Errors and warnings delivered by this class and its subclasses will be
   in a format similar to those produced by gcc(1), containing the filename
   and line number that triggers the error. In some cases, simulation errors
   may result from problems with the input data. Unfortunately, these types
   of errors will not have source information.
   '''
   def __init__(self, filename, fh, simobj, fmt_num, prefix):
      '''
      @param filename: Name of the open input file
      @type filename: str
      @param fh: Handle to the open input file (r mode)
      @param simobj: Reference to the simulator object
      @param fmt_num: Format number of the input file, to be checked against
                    any "format" commands inside the file. When formats do not
                    match, a warning will be generated.
      @type fmt_num: float
      @param prefix: Prefix to add to metadata fields when creating metadata
                     results for the simulator
      @type prefix: str
      '''
      self.filename = filename
      self.fh = fh
      self.metadata = {}
      self.version = None
      self.sim = simobj
      self.fmt_num = fmt_num
      self.prefix = prefix
      self.clock_seen = False
      self.eof = False
      self.line = ''
      self.line_number = 0
      self.parts = []
      self.parse_map = { 'format': self.h_format, 'meta': self.h_meta, \
                         'clock': self.h_clock, 'version': self.h_version, \
                         'epoch': self.h_epoch }
      self.time = 0
      self.epoch_seen = False
      self.epoch = 0
      self.offset = 0
      self.pending_event = None
      self.metadata_results_posted = False
      self.parse()
      #
   def add_handler(self, cmd, handler):
      '''
      Adds a parse handler to support new commands. This method is used by
      subclasses to extend the parser.

      @param cmd: Command to handle
      @type cmd: str
      @param handler: Handler method to invoke when the command is seen
      '''
      self.parse_map[cmd] = handler
   def error(self, msg):
      '''
      Uniform mechanism for producing error messages (ReaderExceptions) with
      filename and line number information. This method automatically adds
      the location information, so the error message passed to this method
      should be "plain" (without filename or line number).

      @param msg: Error message to include
      @type msg: str
      '''
      raise ReaderException('%s:%d: error: %s' % \
         (self.filename, self.line_number, msg))
      #
   def warning(self, msg):
      '''
      Uniform mechanism for raising a warning when conditions exist in the
      input file that do not necessarily constitute an error. Location
      information (filename and line number) are added automatically.

      @param msg: Warning message to include
      @type msg: str
      '''
      raise Warning('%s:%d: warning: %s' % \
         (self.filename, self.line_number, msg))
      #
   def scan(self):
      '''
      Implements the scanner portion of the common format reader. This method
      is invoked automatically by the parse() method and sets the current
      scan line (self.line) and token list (self.parts).

      @rtype: bool
      @return: True if there is more data in the input file, False if the
               reader has reached EOF
      '''
      self.line = ''
      self.parts = []
      while self.line == '' and not self.eof:
         raw = self.fh.readline()
         if raw == '':   # EOF
            self.eof = True
         else:
            self.line = raw.strip().split('#')[0].replace('\\P', '#')
            if self.line != '':
               self.parts = self.line.split()
         self.line_number += 1
      return not self.eof
   def parse(self):
      '''
      Main parser loop, invoked by the trigger() method. The parser loop runs,
      invoking the scanner and parse handlers as needed, until a parse handler
      returns False or EOF is reached. In general, only h_clock() ever returns
      False, to indicate that parsing of the current clock block is complete.
      '''
      run = not self.eof
      while run:
         run = self.scan()
         if run:
            cmd = self.parts[0]
            if cmd not in self.parse_map:
               self.error('Unknown command: ' + cmd)
            run = self.parse_map[cmd]()
         else:
            # At EOF: dump metadata if we have not yet done so
            if not self.metadata_results_posted:
               for key in self.metadata:
                  name = self.prefix + key
                  value = self.metadata[key]
                  self.sim.add_result(MetadataResult(name, value))
               #
               if self.version is not None:
                  name = self.prefix + 'version'
                  value = self.version
                  self.sim.add_result(MetadataResult(name, value))
               #
               self.metadata_results_posted = True
         #
      #
   def h_format(self):
      '''
      Parse handler for the "format" command. Raises a warning if the format
      of the input file does not match the format number set in the
      constructor.

      @return: True
      '''
      if len(self.parts) != 2:
         self.error('format command takes exactly 1 argument')
      try:
         fmt = float(self.parts[1])
      except Exception, msg:
         self.error(msg)
      if fmt != self.fmt_num:
         self.warning('format %s differs from reader version %s' % \
         (str(fmt), str(self.fmt_num)))
      return True
   def h_meta(self):
      '''
      Parse handler for metadata. Each metadata item is added to the
      instance metadata dictionary (self.metadata).

      @return: True
      '''
      metacmd = self.line[4:].strip()
      parts = metacmd.split('=')
      if len(parts) != 2:
         self.error('malformed meta statement')
      name, value = parts
      self.metadata[name] = value
      return True
   def h_version(self):
      '''
      Parse handler for file version number.

      @return: True
      '''
      if len(self.parts) != 2:
         self.error('version command takes exactly 1 argument')
      try:
         self.version = float(self.parts[1])
      except Exception, msg:
         self.error(msg)
      return True
   def h_epoch(self):
      '''
      Parse handler for the epoch statement, which allows multiple input
      files with normalized times to be used in the same simulation. An epoch
      statement may only occur once in the file, and it must occur before the
      first clock line.

      @return: True
      '''
      if len(self.parts) != 2:
         self.error('epoch statement takes exactly 1 argument')
      if self.epoch_seen:
         self.error('epoch statement may only occur once in a file')
      if self.clock_seen:
         self.error('epoch statement must occur before first clock line')
      try:
         ef = float(self.parts[1])
         ei = int(ef)
         self.epoch = ef
         if ei == ef:
            self.epoch = ei
         self.epoch_seen = True
      except Exception, msg:
         self.error(msg)
      return True
   def h_clock(self):
      '''
      Parse handler for clock lines. Upon reading a clock line, this handler
      will schedule a trigger() method invocation for the time specified by
      the clock line.

      @return: False
      '''
      if len(self.parts) != 2:
         self.error('clock command takes exactly 1 argument')
      try:
         tsf = float(self.parts[1])
      except Exception, msg:
         self.error(msg)
      tsi = int(tsf)
      timestamp = tsf
      if tsi == tsf:
         timestamp = tsi
      self.add_trigger(timestamp)
      self.clock_seen = True
      return False
   def in_clock_block(self):
      '''
      @return: True iff at least one clock line has been parsed, allowing
               routines to ensure that the current command is inside a clock
               block
      '''
      return self.clock_seen
   def add_trigger(self, time):
      '''
      Adds a new parse trigger as a simulator event scheduled for a time not
      in the past. Only one trigger event may be active at a time: adding a
      second trigger will result in an Exception being raised.

      @param time: Time at which the parse trigger will be invoked, subject
                   to automatic offset
      @type time: number
      '''
      if self.pending_event is not None:
         raise Exception('Only 1 trigger event may be pending at any time')
      self.pending_event = \
            self.sim.new_simple_event(time + self.offset, self.trigger)
   def trigger(self, time, simobj):
      '''
      Parse trigger: a simulator-aware function that invokes the parser to
      handle the next clock block.

      @param time: Current simulated time
      @type time: number
      @param simobj: Reference to the simulator object
      '''
      self.pending_event = None
      self.time = time
      self.parse()
   def get_epoch(self):
      '''
      @rtype: number
      @return: epoch value found in the input file (defaults to zero if
               not present)
      '''
      return self.epoch
   def set_offset(self, offset):
      '''
      Sets the clock block time offset, used in cases where multiple input
      files with normalized times are used in the same simulation (see the
      set_normalization() method).

      @param offset: New offset value
      @type offset: number
      '''
      self.offset = offset
      if self.pending_event is not None:
         time = self.pending_event.time
         self.sim.cancel_event(self.pending_event)
         self.pending_event = None
         self.add_trigger(time)   # offset is added by add_trigger
   def set_normalization(self, normtime):
      '''
      Sets the offset according to the specified normalized time value. This
      time value must be the lowest epoch from all data sets, otherwise a
      simulation-time Exception will occur.

      @param normtime: Earliest timestamp among all data sets
      @type normtime: number
      '''
      diff = self.epoch - normtime
      self.set_offset(diff)
#


def normalize(readers):
   '''
   Performs a simulation-time normalization on a list of readers, setting
   each reader normalization value to the earliest epoch found among the
   list members.

   @param readers: List of CommonReader (or child class) objects, each of
                   which must already be initialized on an open input file
   '''
   early_epoch = -1
   for reader in readers:
      epoch = reader.get_epoch()
      if (early_epoch < 0) or ((epoch < early_epoch) and (epoch > 0)):
         early_epoch = epoch
   #
   if early_epoch >= 0:
      for reader in readers:
         reader.set_normalization(early_epoch)
   #
#
